﻿// ----------------------------------------------------------------------------------
// Microsoft Developer & Platform Evangelism
// 
// Copyright (c) Microsoft Corporation. All rights reserved.
// 
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 
// OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// ----------------------------------------------------------------------------------
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
// ----------------------------------------------------------------------------------

namespace Microsoft.Samples.WindowsPhoneCloud.StorageClient
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Net;
    using System.Net.Browser;
    using Microsoft.Samples.WindowsPhoneCloud.StorageClient.Credentials;
    using Microsoft.Samples.WindowsPhoneCloud.StorageClient.Serializers;

    public class CloudBlobClient : ICloudBlobClient
    {
        private readonly ISharedAccessSignatureServiceClient sasService;
        private readonly string baseContainerName;

        private Uri containerUri;
        private ICloudBlobFactory cloudBlobUploaderFactory;
        private bool containerExists = false;

        /// <summary>
        /// Initializes a new instance of the CloudBlobClient class to perform operations with Blobs and Containers using Shared Access Signatures.
        /// </summary>
        /// <param name="sasService">A valid Shared Access Signature service client instance.</param>
        /// <param name="baseContainerName">The blob container name used as base for getting shared access signatures.</param>
        public CloudBlobClient(ISharedAccessSignatureServiceClient sasService, string baseContainerName)
        {
            if (sasService == null)
            {
                throw new ArgumentNullException("sasService", "The Shared Access Signature service client cannot be null.");
            }

            if (string.IsNullOrEmpty(baseContainerName))
            {
                throw new ArgumentNullException("baseContainerName", "The base container name cannot be null nor empty.");
            }

            this.sasService = sasService;
            this.baseContainerName = baseContainerName;
        }

        /// <summary>
        /// Initializes a new instance of the CloudBlobClient class to perform operations with Blobs and Containers using Windows Azure Storage credentials directly.
        /// </summary>
        /// <param name="baseContainerAddress">The blob container endpoint.</param>
        /// <param name="credentials">The account credentials.</param>
        public CloudBlobClient(string baseContainerAddress, IStorageCredentials credentials = null)
        {
            this.containerUri = new Uri(baseContainerAddress, UriKind.Absolute);
            this.StorageCredentials = credentials ?? new StorageCredentialsAnonymous();
        }

        public IStorageCredentials StorageCredentials { get; set; }

        internal ICloudBlobFactory CloudBlobFactory
        {
            get
            {
                return this.cloudBlobUploaderFactory != null ? this.cloudBlobUploaderFactory : new CloudBlobFactory();
            }

            set
            {
                this.cloudBlobUploaderFactory = value;
            }
        }

        public void Upload(string blobName, Stream fileStream, Action<CloudOperationResponse<bool>> callback)
        {
            if ((this.containerUri != null) && !this.IsContainerSASExpired())
            {
                this.InnerUpload(blobName, fileStream, callback);
            }
            else
            {
                if (this.sasService != null)
                {
                    this.UploadUsingSasService(blobName, fileStream, callback);
                }
                else
                {
                    this.CreateContainerIfNotExist<bool>(() => this.InnerUpload(blobName, fileStream, callback), callback);
                }
            }
        }

        public void ListBlobsWithPrefix(string prefix, bool useFlatBlobListing, Action<CloudOperationResponse<IEnumerable<ICloudBlob>>> callback)
        {
            if (this.sasService != null)
            {
                this.sasService.GetBlobsSharedAccessSignatures(this.baseContainerName, prefix, useFlatBlobListing, callback);
            }
            else
            {
                this.CreateContainerIfNotExist<IEnumerable<ICloudBlob>>(() => this.InnerListBlobsWithPrefixUsingStorageKey(prefix, useFlatBlobListing, callback), callback);
            }
        }

        private void UploadUsingSasService(string blobName, Stream fileStream, Action<CloudOperationResponse<bool>> callback)
        {
            this.sasService.GetContainerSharedAccessSignature(
                this.baseContainerName,
                r =>
                {
                    if (r.Exception == null)
                    {
                        this.containerUri = r.Response;
                        this.InnerUpload(blobName, fileStream, callback);
                    }
                    else
                    {
                        callback(new CloudOperationResponse<bool>(false, r.Exception));
                    }
                });
        }
       
        private void CreateContainerIfNotExist<T>(Action successCallback, Action<CloudOperationResponse<T>> errorCallback)
        {
            if (!this.containerExists)
            {
                var container = this.CloudBlobFactory.CreateCloudBlobContainer(this.StorageCredentials);
                container.CreateContainerIfNotExist(
                     this.containerUri,
                     (e) => 
                        {
                            if (e.Exception == null)
                            {
                                this.containerExists = true;
                                successCallback();
                            }
                            else
                            {
                                errorCallback(new CloudOperationResponse<T>(default(T), e.Exception));
                            }
                        });
            }
            else
            {
                successCallback();
            }
        }

        private void InnerListBlobsWithPrefixUsingStorageKey(string blobPrefix, bool useFlatBlobListing, Action<CloudOperationResponse<IEnumerable<ICloudBlob>>> callback)
        {
            var serviceOperation = string.Concat(this.containerUri, "?restype=container&comp=list&timeout=90");

            if (!useFlatBlobListing)
            {
                serviceOperation = string.Concat(serviceOperation, "&delimiter=/");
            }

            if (!string.IsNullOrEmpty(blobPrefix))
            {
                serviceOperation = string.Concat(serviceOperation, "&prefix=", blobPrefix);
            }

            // Adding an incremental seed to avoid a cached response.
            serviceOperation = string.Concat(serviceOperation, "&incrementalSeed=", DateTime.UtcNow.Ticks);

            var request = (HttpWebRequest)WebRequestCreator.ClientHttp.Create(new Uri(serviceOperation));
            request.Headers["x-ms-version"] = "2009-09-19";
            request.Method = "GET";

            this.StorageCredentials.SignRequest(request, -1);
            request.BeginGetResponse(
                ar =>
                {
                    try
                    {
                        if (request.HaveResponse)
                        {
                            var response = (HttpWebResponse)request.EndGetResponse(ar);
                            var serializer = new StorageAccountBlobDataContractSerializer();
                            var results = serializer.ReadObject(response.GetResponseStream());

                            callback(new CloudOperationResponse<IEnumerable<ICloudBlob>>((results != null) ? results : new ICloudBlob[0], null));
                        }
                    }
                    catch (Exception exception)
                    {
                        callback(new CloudOperationResponse<IEnumerable<ICloudBlob>>(null, StorageClientExceptionParser.ParseXmlWebException(exception as WebException) ?? exception));
                    }
                },
                null);
        }

        private void InnerUpload(string blobName, Stream fileStream, Action<CloudOperationResponse<bool>> callback)
        {
            var uriBuilder = new UriBuilder(this.containerUri);
            uriBuilder.Path += string.Format(CultureInfo.InvariantCulture, "/{0}", blobName.TrimStart('/', '\\'));

            var uploadUrl = uriBuilder.Uri.AbsoluteUri;
            var uploader = this.CloudBlobFactory.CreateCloudBlobUploader(fileStream, uploadUrl, this.StorageCredentials);

            uploader.UploadFinished += (s, r) => callback(r);
            uploader.StartUpload();
        }

        private bool IsContainerSASExpired()
        {
            if (this.containerUri == null)
            {
                return true;
            }

            var se = Credentials.HttpUtility.ParseQueryString(this.containerUri.Query)["se"];
            if (string.IsNullOrEmpty(se))
            {
                return true;
            }

            DateTime expirationDate;
            if (!DateTime.TryParseExact(se, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out expirationDate))
            {
                return true;
            }

            return expirationDate <= DateTime.UtcNow;
        }
    }
}
